/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.ggdi;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.shell.log.ServletContextTreeLogger;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.servlet.GuiceFilter;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.servlet.ServletModule;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
 * The base class for the test servlets.
 *
 * This class implements the logic to dispatch requests to a {@code
 * GuiceFilter} that has been initialized with the Guice {@code Module} needed
 * for the GWT module that is currently being tested.
 *
 * @author jgennis
 */
public abstract class TestServlet extends HttpServlet {

  static ModuleDef moduleDef;

  static ServletContextListener contextListener;

  static GuiceFilter guiceFilter;

  class TestServletFilterConfig implements FilterConfig {

    public String getFilterName() {
      return moduleDef.getName() + " filter";
    }

    public ServletContext getServletContext() {
      return TestServlet.this.getServletContext();
    }

    public String getInitParameter(String name) {
      return TestServlet.this.getInitParameter(name);
    }

    public Enumeration getInitParameterNames() {
      return TestServlet.this.getInitParameterNames();
    }
  }

  class TestServletFilterChain implements FilterChain {

    public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
      // do nothing
    }
  }

  @Override
  public void destroy() {
    if (moduleDef != null) {
      contextListener.contextDestroyed(new ServletContextEvent(getServletContext()));
      guiceFilter.destroy();
      moduleDef = null;
      contextListener = null;
      guiceFilter = null;
    }
    super.destroy();
  }

  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    // If the servlet container matches the URL using a "/*" pattern then
    // request.getServletPath() returns an empty string.  This can't be used to
    // by the GuiceFilter to route the request, so we need to fake it out using
    // request.getPathInfo().
    HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request) {

      @Override
      public String getServletPath() {
        String servletPath = super.getServletPath();
        if (servletPath.equals(""))
          return super.getPathInfo();
        return servletPath;
      }
    };

    TreeLogger treeLogger = new ServletContextTreeLogger(getServletContext());
    String moduleName = getModuleName(request.getPathInfo());
    if (moduleDef == null) {
      try {
        moduleDef = Utils.getModuleDef(treeLogger, moduleName);
        final List<Module> modules = Utils.getGuiceModules(moduleDef);
        List<Module> testModules = Utils.getTestGuiceModules(moduleDef);
        modules.addAll(testModules);
        Map<String, Class<? extends HttpServlet>> bindings = Utils.getServletBindings(treeLogger,
            modules);
        Module servletModule = createServletModule(moduleName, bindings);
        modules.add(servletModule);
        contextListener = new GuiceServletContextListener() {

          @Override
          protected Injector getInjector() {
            return Guice.createInjector(modules);
          }
        };
      } catch (ConfigurationException ex) {
        throw new ServletException(ex);
      }
      contextListener.contextInitialized(new ServletContextEvent(getServletContext()));
      guiceFilter = new GuiceFilter();
      guiceFilter.init(new TestServletFilterConfig());
    } else if (!moduleName.equals(moduleDef.getName()) &&
        !moduleName.equals(moduleDef.getCanonicalName())) {
      throw new RuntimeException("The requested module is not the module that has been " +
          "initialized");
    }

    guiceFilter.doFilter(requestWrapper, response, new TestServletFilterChain());
  }

  String getModuleName(String path) {
    String moduleName;
    int slash = path.indexOf('/', 1);
    if (slash != -1)
      moduleName = path.substring(1, slash);
    else
      moduleName = path.substring(1);
    return moduleName;
  }

  String getRelativePath(String path) {
    String relativePath;
    int slash = path.indexOf('/', 1);
    if (slash != -1)
      relativePath = path.substring(slash);
    else
      relativePath = "";
    return relativePath;
  }

  Module createServletModule(final String moduleName,
      final Map<String, Class<? extends HttpServlet>> bindings) {
    ServletModule servletModule = new ServletModule() {

      @Override
      protected void configureServlets() {
        /*
         * Because we're merging multiple module URIs down to a single JUnit
         * module path we need to check to make sure that the merging doesn't
         * cause conflicts.
         */
        Map<String, Class<? extends HttpServlet>> boundServlets =
            new HashMap<String, Class<? extends HttpServlet>>();
        for (Map.Entry<String, Class<? extends HttpServlet>> binding : bindings.entrySet()) {
          String path = binding.getKey();
          String relativePath = getRelativePath(path);
          String modulePath = "/" + moduleName + relativePath;
          Class<? extends HttpServlet> servlet = binding.getValue();
          Class<? extends HttpServlet> boundServlet = boundServlets.get(modulePath);
          if (boundServlet == null) {
            boundServlets.put(modulePath, servlet);
            serve(modulePath).with(servlet);
          } else if (boundServlet != servlet) {
            throw new RuntimeException("Multiple servlet mappings found for path '" + modulePath +
                "':\n  " + boundServlet.getCanonicalName() + "\n  " + servlet.getCanonicalName());
          }
        }
      }
    };
    return servletModule;
  }
}
